package com.southgis.ibase.sql.utils;

import com.southgis.ibase.sql.config.DataSourceDefineTemplate;
import com.southgis.ibase.sql.func.ColumnDefine;
import com.southgis.ibase.sql.func.CommonDialect;
import com.southgis.ibase.sql.func.CommonFunc;
import com.southgis.ibase.sql.func.IExclusiveDatabaseFunc;
import com.southgis.ibase.sql.func.IExclusiveDatabaseFunc.ConstraintTypeEnum;
import com.southgis.ibase.sql.func.IExclusiveDatabaseFunc.TableTypeEnum;
import com.southgis.ibase.sql.func.ModifyColumnDefine;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.hibernate.dialect.Dialect;

import javax.sql.DataSource;
import java.util.List;

/**
 * 根据配置的数据库类型，生成相应的sql函数调用语句
 * @author dennis
 */
@Slf4j
public class DatabaseFunc {

	//==根据各种信息构造处理类==
	static DatabaseFunc defaultDbFuncBuilder;
	
	/**
	 * 获取默认数据库配置项的单例对象
	 * @return
	 */
	public static DatabaseFunc getInstance() {
		if(defaultDbFuncBuilder==null)
			defaultDbFuncBuilder=createInstanceByTypeName(DatabaseUtil.getDataSourceType());
		
		return defaultDbFuncBuilder;
	}

	/**
	 * 从数据源获取指定数据驱动配置项得到数据库类型（一般在多数据源的情况下调用）。
	 * （注意：每次调用，都会创建一个新对象）
	 * @param dataSource 数据源
	 */
	public static DatabaseFunc createInstanceByDataSource(DataSource dataSource) {
		return createInstanceByTypeName(DatabaseUtil.getDataSourceType(dataSource));
	}
	
	/**
	 * 根据指定数据库类型，创建兼容语句生成器（注意：每次调用，都会创建一个新对象）
	 * @param dbType 数据库类型名，在DataSourceType中定义的字符串
	 * @return
	 */
	public static DatabaseFunc createInstanceByTypeName(String dbType) {
		return new DatabaseFunc(dbType.toLowerCase());
	}
	//=================================
	
	/**初始化及内部属性*/

	/**
	 * 有参构造
	 * @param dbType 数据源类型名，在DataSourceType中定义的字符串
	 */
	private DatabaseFunc(String dbType) {
		this.dbType = dbType;
	}

	/**
	 * 数据源类型名，在DataSourceType中定义的字符串
	 */
	private String dbType;
	/**
	 * 获取当前数据源类型名，在DataSourceType中定义的字符串
	 * @return
	 */
	public String getDbType()
	{
		return dbType;
	}

	/**
	 * 数据源定义模板类
	 */
	private DataSourceDefineTemplate dbDefineTemplate;
	/**
	 * 获取当前数据源定义模板类
	 * @return
	 */
	public DataSourceDefineTemplate getDbDefineTemplate()
	{
		if(dbDefineTemplate==null) {
			dbDefineTemplate = DatabaseUtil.getDataSourceTemplate(getDbType());
		}
		return dbDefineTemplate;
	}

	/**
	 * 数据源方言类对象
	 */
	private Dialect dbDialect;
	/**
	 * 获取当前数据源方言类
	 * @return
	 */
	public Dialect getDbDialect()
	{
		if(dbDialect==null) {
			String clsName=getDbDefineTemplate().getDialect();
			try {
				Class<?> clazz=Thread.currentThread().getContextClassLoader().loadClass(clsName);
				dbDialect=(Dialect)clazz.newInstance();
			}catch (Exception e) {
				dbDialect=new CommonDialect();
				log.warn("配置的sql方言类不存在:"+clsName, e);
			}
		}
		return dbDialect;
	}

	/**
	 * 数据源SQL片段处理类对象
	 */
	private IExclusiveDatabaseFunc exclusiveFunc;
	/**
	 * 获取当前数据源SQL片段处理类
	 * @return
	 */
	public IExclusiveDatabaseFunc getExclusiveFunc()
	{
		if(exclusiveFunc==null) {
			String clsName=getDbDefineTemplate().getFuncClassName();
			Dialect curDialect=getDbDialect();
			try {
				//用线程上下文中的加载器加载，从而可以优先使用ext外部的类
				Class<?> clazz=Thread.currentThread().getContextClassLoader().loadClass(clsName);
				exclusiveFunc=(IExclusiveDatabaseFunc)clazz.newInstance();
			}catch (Exception e) {
				exclusiveFunc=new CommonFunc();
				log.warn("配置的sql功能类不存在:"+clsName, e);
			}
			exclusiveFunc.setDialect(curDialect,dbType);
		}
		return exclusiveFunc;
	}
	//===============================
	
	
	/**
	* 根据数据库类型称获取数据库驱动类名
	* @return
	*/
	public String getDriverClassName() {
		return getDbDefineTemplate().getDriverClassName();
	}

	/**
	 * 基于当前数据库类型生成连接串
	 * @param host 地址
	 * @param port 端口
	 * @param dbName 数据库名称
	 * @return 返回数据库连接串
	 */
	public String formatUrl(String host,String port,String dbName)
	{
		return getDbDefineTemplate().formatUrl(host, port, dbName);
	}

	//README ==常用字段类型定义==
	/**
	 * 基于当前数据库类型返回正确的SQL定义字符类型的类型名。
	 * @param len 定义长度。如果小于0，则返回大文本类型；等于0，则仅返回类型，不包含长度定义；大于0，则返回包含长度定义的类型
	 * 如果指定了长度，但超过了数据库最大长度限制，会自动以大文本串类型返回。
	 * @return 如：CLOB、varchar、varchar(100)
	 */
	public String charType(int len)
	{
		return getExclusiveFunc().charType(len);
	}

	/**
	 * 基于当前数据库类型返回正确的SQL定义二进制器类型的类型名。
	 * @param len 定义长度。如果小于0，则返回大二进制块类型；等于0，则仅返回类型，不包含长度定义；大于0，则返回包含长度定义的类型
	 * @return 如：BLOB、raw、raw(100)
	 */
	public String binaryType(int len)
	{
		return getExclusiveFunc().binaryType(len);
	}

	/**
	 * 基于当前数据库类型返回正确的SQL定义日期类型的类型名。
	 * @return 如：date、datetime、timestamp
	 */
	public String datetimeType()
	{
		return getExclusiveFunc().datetimeType();
	}
	
	/**
	 * 基于当前数据库类型返回正确的SQL定义浮点小数类型的类型名。
	 * @return 如：double、real
	 */
	public String doubleType()
	{
		return getExclusiveFunc().doubleType();
	}
	
	/**
	 * 基于当前数据库类型返回正确的SQL定义定点小数类型的类型名。
	 * @param len 数字长度，如果<=0，则会退化返回为doubleType()
	 * @param scale 保留小数位
	 * @return
	 */
	public String decimalType(int len,int scale)
	{
		if(len<=0) return doubleType();

		return getExclusiveFunc().decimalType(len,scale);
	}

	/**
	 * 基于当前数据库类型返回正确的SQL定义整数类型的类型名。
	 * @param width 保存值的宽度（字节数），目前支持：1=字节型byte、2=短整数smallint、4=整数int、8=长整数bigint、16=大整数decimal(64,0)
	 * @return 如：byte、int、bigint
	 */
	public String intType(int width)
	{
		return getExclusiveFunc().intType(width);
	}

	//====================================
	
	//README ==常用SQL语句片段（函数调用等）==
	/**
	 * 生成非空字符串判断条件
	 * @param fieldName （字符串类型的）字段名
	 * @return 如oracle库返回：(fieldName is not null)，
	 * 通用实现返回：(fieldName is not null and fieldName<>'')
	 */
	public String notEmptyCharWhere(String fieldName) {
		return getExclusiveFunc().notEmptyCharWhere(fieldName);
	}
	
	/**
	 * 字符串空条件子句
	 * @param fieldName 字段名
	 * @return 如oracle库返回：(fieldName is null)，
	 * 通用实现返回：(fieldName is null or fieldName='')
	 */
	public String emptyCharWhere(String fieldName)
	{
		return getExclusiveFunc().emptyCharWhere(fieldName);
	}

	/**
	 * 连接字符串值
	 * @param charVals （sql char类型）的标量值或字段名或SQL表达式
	 * @return 通用实现返回：CONCAT(charVal1,charVal2,...)
	 */
	public String concatChar(String... charVals) {
		return getExclusiveFunc().concatChar(charVals);
	}

	/**
	 * 截取字符串类型值的一部分。返回sql表达式片段。
	 * @param charVal 要截取内容的sql表达式
	 * @param start 截取内容的开始位置（基于0开始）
	 * @param end 截取内容的结束位置（基于0开始），但不包含此位置的字符。
	 * @return
	 */
	public String subChar(String charVal, int start, int end)
	{
		return getExclusiveFunc().subChar(charVal, start, end);
	}
	
	/**
	 * 生成“将字符串转换为日期值”的SQL函数调用。如果使用默认格式，对于oracle，需要设置默认日期格式：<br>
	 * 会话级设置：alter session set nls_date_format='yyyy-mm-dd HH24:mi:ss'<br>
	 * 系统级设置：alter system set nls_date_format='yyyy-mm-dd hh24:mi:ss' scope=spfile<br>
	 * @param charVal （sql char类型）的SQL表达式，是标准写法的日期字符串。如：
	 * "'2018-12-12'"、或："'2018-12-12 21:30:30'" 或 "'2020-'+FMonth+'-'+FDAY"
	 * @param fmt 转换值格式（-1默认格式；<br>
	 * 0 yyyy-MM-dd HH:mm:ss；1 yyyyMMddHHmmss；<br>
	 * 10 yyyy-MM-dd；11 yyyyMMdd）
	 * @return 调用语句的SQL片段
	 */
	public String getCharToDateInvoke(String charVal, int fmt) {
		return getExclusiveFunc().getCharToDateInvoke(charVal, fmt);
	}

//	/**
//	 * 生成“将日期转换为字符串值”的SQL函数调用
//	 * @param dateVal （sql datetime类型）的SQL表达式。
//	 * @param fmt 转换值格式（-1 默认格式，即不用格式串；<br>
//	 * 0 yyyy-MM-dd HH:mm:ss；1 yyyyMMddHHmmss；<br>
//	 * 10 yyyy-MM-dd；11 yyyyMMdd）
//	 * @return 调用语句的SQL片段
//	 */
//	public String getDateToCharInvoke(String dateVal, int fmt) {
//		return getExclusiveFunc().getDateToCharInvoke(dateVal, fmt);
//	}

	/**
	 * 生成“获取年份”的SQL函数调用，通用实现使用注册的year函数。
	 * @param dateVal （sql date类型）的标量值或字段名或SQL表达式
	 * @return 调用语句的SQL片段
	 */
	public String getYearInvoke(String dateVal) {
		return getExclusiveFunc().getYearInvoke(dateVal);
	}
	
	/**
	 * 得到SQL日期常量值的表示方法，如：<br>
	 * String dateVal=func.getDateConst("2020-12-31");<br>
	 * 一般sql类型，返回：“date'2020-12-31'”<br>
	 * mssql，返回：“'2020-12-31'”
	 * @param fmtVal 系统标准日期格式的sql常量字符串（注，也可省略单引号，因为已明确此参数是常量，如果省略，内部会自动添加）
	 * @return sql日期常量值的表示
	 */
	public String getDateConst(String fmtVal)
	{
		return getExclusiveFunc().getDateConst(fmtVal);
	}
	
	/**
	 * 生成“值类型转换”的SQL函数调用
	 * @param orgVal 转换原值的SQL表达式
	 * @param targetType 转换后的类型（java.sql.Types定义值）
	 * @param length 转换结果长度（如果转换类型不需要长度，比如int类型，可设置为-1）
	 * @param scale 转换结果精度（如果转换类型不需要精度，比如int类型，可设置为-1）
	 * @return 通用实现调用Dialect.cast
	 */
	public String getCastInvoke(String orgVal,int targetType,int length, int scale) {
		return getExclusiveFunc().getCastInvoke(orgVal,targetType,length,scale);
	}

	/**
	 * 生成“转换为小写”的SQL函数调用
	 * @param orgVal 转换原值的SQL表达式
	 * @return
	 */
	public String getLowerInvoke(String orgVal)
	{
		return getExclusiveFunc().getLowerInvoke(orgVal);
	}

	/**
	 * 生成“转换为大写”的SQL函数调用
	 * @param orgVal 转换原值的SQL表达式
	 * @return
	 */
	public String getUpperInvoke(String orgVal)
	{
		return getExclusiveFunc().getUpperInvoke(orgVal);
	}	
	/**
	 * 生成“如果null返回另一个值”的SQL函数调用
	 * @param orgVal 转换原值的SQL表达式
	 * @param other 如果orgVal为空时的替换值
	 * @return
	 */
	public String getIfnullInvoke(String orgVal, String other)
	{
		return getExclusiveFunc().getIfnullInvoke(orgVal, other);
	}
	
	/**
	 * 当前时间的函数调用，注：某些数据库类型返回的函数调用不带括号。
	 * 如：oracle返回sysdate，mysql返回now()
	 * @return
	 */
	public String getNowInvoke()
	{
		return getExclusiveFunc().getNowInvoke();
	}

	//============================================

	//README ==生成分页语句==
	/**
	 * 对查询添加分页信息。如果不支持分页，则返回null，由调用者自行处理
	 * @param sql 要添加的原始sql语句，为了分页不混乱，sql一般应添加order by子句。
	 * @param offset 分页开始位置，基于0开始。如果小于0修正为0。
	 * @param pageSize 分页记录大小，如果不大于0默认为50。
	 * @return 添加了分页信息的sql语句，如果不支持分页，则返回null。
	 */
	public String pageSql(String sql, int offset, int pageSize) {
		if(offset<0)
			offset=0;
		if(pageSize<=0)
			pageSize=50;
		return getExclusiveFunc().pageSql(sql, offset, pageSize);
	}
	
	/**
	 * 生成获取总数的语句
	 * @param sql
	 * @return
	 */
	public String countSql(String sql) {
		return getExclusiveFunc().countSql(sql);
	}
	//============================================

	//README ==元数据操作DDL语句==
	
	/**
	 * 获取指定表名的数量（可用于判断表是否存在）
	 * @param tableName 表名
	 * @param schema 数据库Schema，不能为空
	 * @return 用于执行获取指定表名的数目的SQL语句，执行SQL后返回结果为：[{"COUNT":1}]
	 */
	public String sqlOfTableCount(String tableName, String schema) {
		return getExclusiveFunc().sqlOfTableCount(tableName, schema);
	}
	
	/**
	 * 获取所有表名，返回内容：表名、表注释、表类型（1=Table，2=View）
	 * @param name 查找的匹配名条件（根据type不同，用like（比如："JOB_%"），或相等（比如："JOB_BASE"）。可以为空。
	 * 注意：如果内容包含_，内部会做处理，视为原本字符，而不是like的通配符。
	 * @param type 表类型（按位组合）：0不设条件，1表、2视图，3表和视图
	 * @param schema 数据库Schema，不能为空
	 * @return 用于执行获取所有表信息的SQL语句，执行SQL后返回结果为: [{"TABLE_NAME":"joba", "TABLE_COMMENT":"这是表","TABLE_TYPE":1}, ...]
	 */
	public String sqlOfListTable(String name, TableTypeEnum type, String schema) {
		return getExclusiveFunc().sqlOfListTable(name,type,schema);
	}

	/**
	 * 根据表名和Schema获取表字段和类型的SQL，返回内容：列字，注释，是否可空（Y,N），数据类型，长度，小数位，长度定义。<br>
	 * 其中：【长度】，【小数位】都为-1值时，应通过【长度定义】分析长度与小数位，格式：xxx(长度)yyy，或：xxx(长度,小数位)yyy。
	 * 可使用{@link CommonFunc#getColnumLengthFromDefine}转换
	 * @param tableName 要获取字段信息的表名（如果表名带%符，则用模糊查询），如果为空，则获取所有表字段。
	 * 注意：如果内容包含_，内部会做处理，视为原本字符，而不是like的通配符。
	 * @param schema 数据库Schema，不能为空
	 * @return 用于执行获取表字段信息的SQL语句，执行SQL后返回结果为: [{"TABLE_NAME":"joba","COLUMN_NAME":"id","COLUMN_COMMENT":"描述",
	 *  "COLUMN_NULLABLE":"Y","DATA_TYPE":"varchar","DATA_LENGTH":200,"DATA_SCALE":null,"DATA_DEFINE":null}, ...]
	 */
	public String sqlOfListColumns(String tableName, String schema) {
		return getExclusiveFunc().sqlOfListColumns(tableName, schema);
	}

	/**
	 * 根据表名获取约束定义的SQL（可用于判断主键是否存在、外键是否存在等），返回内容：约束名，约束类型（1=主键、2=唯一、4=外键），约束字段名
	 * @param type 约束类型（按位组合）：0不设条件，1主键、2唯一、4外键，3主键和唯一，5主键和外键，6唯一和外键，7主键和唯一及外键
	 * @param tableName 表名
	 * @param schema 数据库Schema，不能为空
	 * @return 用于执行获取主键定义信息的SQL语句，执行SQL后返回结果为: [{"CONS_NAME":"pk_34324ewa3","CONS_TYPE":1,"COLUMN_NAME":"RID"}, ...]
	 */
	public String sqlOfListConstraint(ConstraintTypeEnum type, String tableName,String schema) {
		if(StringUtils.isEmpty(tableName)){
			return null;
		}
		return getExclusiveFunc().sqlOfListConstraint(type, tableName,schema);
	}

	/**
	 * 创建一个表
	 * @param tableName 表名称
	 * @param comment 表描述
	 * @param colDef 列描述
	 * @param schema 数据库Schema，可选
	 * @return
	 */
	public List<String> sqlOfCreateTable(String tableName, String comment, List<ColumnDefine> colDef, String schema)
	{
		return getExclusiveFunc().sqlOfCreateTable(tableName, comment, colDef, schema);
	}
	
	/**
	 * 修改表的注释
	 * @param comment 注释内容
	 * @param tableName 所修改的目标表
	 * @param schema 数据库Schema，可选
	 * @return
	 */
	public String sqlOfModifyTableComment(String comment,String tableName,String schema)
	{
		return getExclusiveFunc().sqlOfModifyTableComment(comment, tableName, schema);
	}

	/**
	 * 为表添加一个字段
	 * @param colDef 列描述
	 * @param tableName 表名称
	 * @param schema 数据库Schema，可选
	 * @return
	 */
	public List<String> sqlOfAddColumn(ColumnDefine colDef,String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfAddColumn(colDef, tableName, schema);
	}

	/**
	 * 修改表字段定义
	 * @param colDef 修改的字段信息。
	 * 注意，为了兼容，如果不修改某些信息时，需要设置正确的原字段信息。否则mysql这类数据库无法正确修改其他内容。<br>
	 * 调用示例：
	 * <pre><code>
	 * ModifyColumnDefine colDef=new ModifyColumnDefine();
	 * 	／／设置原字段定义信息（如果对象需要重用，在每次设置前应调用resetModify重置）
	 * 	／／colDef.resetModify();
	 * colDef.setName("rid");
	 * colDef.setType(ColumnTypeEnum.Text);
	 * colDef.setLength(50);
	 * colDef.setNullable(false);
	 * colDef.setComment("主键");
	 * 	／／设置要修改的属性
	 * colDef.modifyNullable(false)
	 * 	.modifyType(ColumnTypeEnum.Bigint, 0, 0, null);
	 * <code></pre>
	 * @param tableName
	 * @param schema 数据库Schema，可选
	 * @return
	 */
	public List<String> sqlOfModifyColumn(ModifyColumnDefine colDef,String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfModifyColumn(colDef, tableName, schema);
	}

	/**
	 * 删除一个字段（使用此方法时需要特别小心，仅在使用重命名的方式改字段类型时使用），否则一旦误删除，会导致数据破坏。
	 * @param colName 要删除的字段名
	 * @param tableName 表名
	 * @param schema 架构名
	 * @return
	 */
	public String sqlOfDropColumn(String colName, String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfDropColumn(colName, tableName, schema);
	}
	
	/**添加主键，是标准sql：ALTER TABLE {table} ADD CONSTRAINT {constraint} PRIMARY KEY({fields})。
	 * 
	 * @param keyName 主键命名，如果为空，按规则自动生成：PK时间戳
	 * @param colName 要设为主键的字段名
	 * @param tableName 表名
	 * @param schema 架构名
	 * @return
	 */
	public String sqlOfAddPrimaryKey(String keyName, String colName, String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfAddPrimaryKey(keyName, colName, tableName, schema);
	}

	/**
	 * 删除主键的sql语句。
	 * @param keyName 主键名
	 * @param tableName 表名
	 * @param schema 数据库Schema，可选
	 * @return 通用实现返回：ALTER TABLE {schema}.{tableName} drop CONSTRAINT {keyName}
	 */
	public String sqlOfDropPrimaryKey(String keyName, String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfDropPrimaryKey(keyName, tableName, schema);
	}
	
	/**
	 * 添加唯一约束，是标准sql：ALTER TABLE {table} ADD CONSTRAINT {constraint} UNIQUE({fields})。
	 * @param keyName 唯一约束名
	 * @param colName 添加约束的字段名
	 * @param tableName 所在表名
	 * @param schema 架构名
	 * @return
	 */
	public String sqlOfAddUniqueKey(String keyName, String colName, String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfAddUniqueKey(keyName, colName, tableName, schema);
	}

	/**
	 * 删除唯一约束的sql语句
	 * @param keyName 唯一约束名
	 * @param tableName 所在表名
	 * @param schema 数据库Schema，可选
	 * @return 通用实现返回：ALTER TABLE {schema}.{tableName} drop CONSTRAINT {keyName}
	 */
	public String sqlOfDropUniqueKey(String keyName, String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfDropUniqueKey(keyName, tableName, schema);
	}
	
	/**
	 * 删除外键的sql语句。不建议添加外键（由系统维护关联，有外键不利于数据维护）。标准sql：<br>
	 * alter table {table} add constraint {constraint} foreign key({fields}) references {reftable}({reffields});
	 * @param keyName 外键名字
	 * @param tableName 所在表名
	 * @param schema 数据库Schema，可选
	 * @return 通用实现返回：ALTER TABLE {schema}.{tableName} drop CONSTRAINT {keyName}
	 */
	public String sqlOfDropForeignKey(String keyName, String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfDropForeignKey(keyName, tableName, schema);
	}

	/**
	 * 禁用/启用外键约束检查
	 * @param keyName 外键名字
	 * @param enabled true为启用，false为禁用
	 * @param tableName 所在表名
	 * @param schema 数据库Schema，可选
	 * @return
	 */
	public String sqlOfEnableForeignKey(String keyName, Boolean enabled, String tableName, String schema)
	{
		return getExclusiveFunc().sqlOfEnableForeignKey(keyName, enabled, tableName, schema);
	}

}
